#ifndef _sdl_loader3ds_h_
#define _sdl_loader3ds_h_

#include "sdl_graphic.h"
#include "sdl_prim.h"
#include "sdl_debug.h"
#include "sdl_indexmap.h"
#include "sdl_world.h"
#include "sdl_resource.h"
#include <string>
#include <fstream>
#include <vector>
#include <cstring>
#include <iomanip>

#include "boost/filesystem.hpp"
#include "boost/filesystem/operations.hpp"

namespace sdl_graphic {

    /**
     * \brief ładowacz plików 3ds
     *
     * \todo zrobić globalne repozytorium danych zamiast otwierać strumienie plikowe
     * \todo ładowanie materiałów, normali, kolorów
     */
    class loader3ds : public sdl_prim,
    depends<sdl_resource_loader>,
    depends<sdl_debug::debug> {
        sdl_resource resource;
        //std::string texture;
        //sdl_ptr<mesh>	model;


        /*
         * pomocnicze dane importowania
         */
        bool do_we_import;
        int objects_read;
        float scale;

        struct vertex {
            float x, y, z;
        };

        struct coordinate {
            float x[3];
            float y[3];
            float z[3];
            float p[3];

            coordinate() {
                x[0] = 1.0;
                y[1] = 1.0;
                z[2] = 1.0;
                x[1] = x[2] = y[0] = y[2] = z[0] = z[1] = p[0] = p[1] = p[2] = 0.0;
            }
        };

        struct triangle {
            unsigned short a, b, c;
        };

        struct texcoord {
            float u, v;
        };

        struct texmap {
            char name[60];
            bool present;

            texmap() : present(false) {
            }
        };

        struct matdesc {
            char name[60];
            unsigned short nfaces;
            unsigned short * faces;

            bool operator==(const matdesc & s) {
                return std::string(name) == std::string(s.name);
            }
        };

        struct color {
            float r, g, b, a;

            color() : r(1.0f), g(1.0f), b(1.0f), a(1.0f) {
            }
        };

        struct mat {
            char name[60];
            color diffuse;
            color ambient;
            color specular;
            float shininess;
            float transparency;
            bool double_sided;
            texmap tex1;
            texmap tex2;
            texmap op;
            texmap shin;
            texmap spec;
            texmap refl;
            texmap bump;

            mat() {
                double_sided = false;
            }

            bool operator==(const mat & s) {
                return std::string(name) == std::string(s.name);
            }

            bool operator==(const std::string & s) {
                return std::string(name) == s;
            }
        };

        struct triangular_mesh {
            char name[256];
            coordinate coord;
            std::vector<vertex> vertices;
            std::vector<triangle> faces;
            std::vector<texcoord> texcoords;
            std::vector<matdesc> facemats;
            std::vector<color> colors;
        };
        std::vector<mat> materials;
        std::vector<triangular_mesh> meshes;

        enum chunk_type {
            version = 0x0002,
            main3ds = 0x4d4d,
            edit3ds = 0x3d3d,
            edit_object = 0x4000,
            global_scale = 0x0100,
            obj_trimesh = 0x4100,
            tri_vertexl = 0x4110,
            tri_facel1 = 0x4120,
            tri_mappingcoords = 0x4140,
            tri_facematl = 0x4130,
            tri_localcoord = 0x4160,
            mat_block = 0xAFFF,
            mat_name = 0xA000,
            mat_amb = 0xA010,
            mat_dif = 0xA020,
            mat_spec = 0xA030,
            mat_shin = 0xA040,
            mat_trans = 0xA050,
            mat_2sided = 0xA081,
            texmap1 = 0xA200,
            texmap2 = 0xA33A,
            op_map = 0xA210,
            bump_map = 0xA230,
            shin_map = 0xA33C,
            spec_map = 0xA204,
            refl_map = 0xA220
        };

        std::ifstream::pos_type read_chunk(std::ifstream & f, unsigned short & id, unsigned int & len) {
            std::ifstream::pos_type sub_end = f.tellg();
            f.read((char*) (&id), sizeof (unsigned short));
            f.read((char*) (&len), sizeof (unsigned int));
            if (f.tellg() < 0) {
                _msg("loader3ds", 5, "błąd odczytu kawałka");
                sub_end = std::ifstream::pos_type(-1);
            } else {
                _msg("loader3ds", 1, sub_end << ": 0x" << std::hex << id << std::dec << " dłg. " << len);
                sub_end += len;
            }
            return sub_end;
        }
        /**
         * docelowy zaimportowany świat
         */
        sdl_ptr<world> imported_world;
    public:

        loader3ds(const std::string & file) :
        sdl_prim(std::string("3ds loader for resource: ") + file),
        resource(give<sdl_resource_loader>()->load(file)),
        objects_read(0),
        imported_world(new world(std::string("scene imported from ") + file)) {
            _msg("loader3ds", 6, "startuję importer 3DS dla " << resource);
            //if(!boost::filesystem::exists(resource)){
            if (!resource) {
                _msg("loader3ds", 100, "nie można znaleźć pliku " << resource);
                do_we_import = false;
            } else do_we_import = true;
        }

        ~loader3ds() {
            for (int k = 0; k < meshes.size(); k++)
                for (int i = 0; i < meshes[k].facemats.size(); i++)
                    delete [] meshes[k].facemats[i].faces;
        }

        sdl_ptr<world> import() {

            if (do_we_import) {
                if (boost::filesystem::is_regular(resource.get_path())) {
                    _msg("loader3ds", 10, "otwieranie " << resource << " o długości " << boost::filesystem::file_size(resource.get_path()));
                }

                typedef std::ifstream::pos_type pos_type;
                _msg("loader3ds", 4, "importowanie: " << name());
                unsigned short c_id;
                unsigned int c_len;

                std::ifstream f(resource.get_path().string().c_str(), std::ios_base::in | std::ios_base::binary);
                while (!f.eof()) {
                    pos_type sub_end = read_chunk(f, c_id, c_len);
                    if (sub_end < 0) break;

                    switch (chunk_type(c_id)) {
                        case version:
                        {
                            unsigned int v = 0;
                            f.read((char*) (&v), sizeof (unsigned int));
                            _msg("loader3ds", 5, "3DS VERSION: " << v);
                        }
                            break;
                        case main3ds:
                            break;
                        case edit3ds:
                            break;
                        case edit_object:
                            read_object(f, sub_end);
                            objects_read++;
                            break;
                        case mat_block:
                            read_materials(f, sub_end);
                            break;
                        case global_scale:
                            f.read((char*) (&scale), sizeof (float));
                            _msg("lodaer3ds", 5, "wczytano jednostkę 1=" << scale);
                            break;
                        default:
                            _msg("loader3ds", 1, f.tellg() << ": 0x" << std::hex << c_id << std::dec << " skok o " << c_len << " ->" << sub_end);
                            f.seekg(sub_end, std::ios_base::beg);
                            break;
                    }
                }
                f.close();
                _msg("loader3ds", 5, name() << ": wczytano " << objects_read << " obiektów");
                //return model(meshes.front());
                for (int i = 0; i < meshes.size(); i++)
                    imported_world->push_back(model(meshes[i]));
                do_we_import = false;
            }
            return imported_world;
        }

        operator sdl_ptr<world>() {
            return import();
        }
    private:

        coordinate read_coordinate(std::ifstream &f) {
            coordinate coord;
            f.read((char*) (coord.x), 3 * sizeof (float));
            f.read((char*) (coord.y), 3 * sizeof (float));
            f.read((char*) (coord.z), 3 * sizeof (float));
            f.read((char*) (coord.p), 3 * sizeof (float));
            return coord;
        }

        float read_percent(std::ifstream &f, std::ifstream::pos_type end) {
            unsigned short c_id;
            unsigned int c_len;
            float ret = 0.0;
            while (f.tellg() < end && !f.fail()) {
                std::ifstream::pos_type sub_end = read_chunk(f, c_id, c_len);
                if (sub_end < 0) break;

                switch (c_id) {
                    case 0x0030: //int format
                    {
                        unsigned short val;
                        f.read((char*) (&val), sizeof (unsigned short));
                        ret = float(val) / 100.0;
                    }
                        break;

                    case 0x0031: //float format
                    {
                        float val;
                        f.read((char*) (&val), sizeof (float));
                        ret = val / 100.0;
                    }
                        break;
                    default:
                        f.seekg(sub_end, std::ios_base::beg);
                        break;
                }
            }
            return ret;
        }

        void read_object(std::ifstream &f, std::ifstream::pos_type end) {
            typedef std::ifstream::pos_type pos_type;
            unsigned short c_id;
            unsigned int c_len;
            char model_name[40];

            char c_char;
            int i = 0;
            do {
                f.read((char*) (&c_char), sizeof (unsigned char));
                model_name[i] = c_char;
                i++;
            } while (c_char != '\0' && i < 40);
            _msg("loader3ds", 5, f.tellg() << ": czytanie obiektu " << model_name << " w kawałku o długości " << end - f.tellg());

            while (f.tellg() < end && !f.fail()) {
                pos_type sub_end = read_chunk(f, c_id, c_len);
                if (sub_end < 0) break;

                switch (chunk_type(c_id)) {
                    case obj_trimesh:
                    {
                        _msg("loader3ds", 5, f.tellg() << ": czytanie siatki trójkątnej dla " << model_name);
                        triangular_mesh m;
                        std::strcpy(m.name, model_name);

                        unsigned short sub_id;
                        unsigned int sub_len;

                        while (f.tellg() < sub_end && !f.fail()) {
                            pos_type sub_sub_end = read_chunk(f, sub_id, sub_len);
                            if (sub_sub_end < 0) break;

                            switch (chunk_type(sub_id)) {
                                case tri_vertexl:
                                    m.vertices = read_vertices(f);
                                    break;
                                case tri_mappingcoords:
                                    m.texcoords = read_texcoords(f);
                                    break;
                                case tri_facel1:
                                    m.faces = read_faces(f);
                                    break;
                                case tri_facematl:
                                    m.facemats.push_back(read_facemat(f));
                                    break;
                                case tri_localcoord:
                                    m.coord = read_coordinate(f);
                                    break;
                                default:
                                    _msg("loader3ds", 1, f.tellg() << ": 0x" << std::hex << sub_id << std::dec << " skok o " << sub_len << " ->" << sub_sub_end);
                                    f.seekg(sub_sub_end, std::ios_base::beg);
                                    break;

                            }
                        }
                        meshes.push_back(m);
                    }
                        break;

                    default:
                        _msg("loader3ds", 1, f.tellg() << ": 0x" << std::hex << c_id << std::dec << " skok o " << c_len << " ->" << sub_end);
                        f.seekg(sub_end, std::ios_base::beg);
                        break;

                }
            }
        }

        void read_materials(std::ifstream & f, std::ifstream::pos_type end) {
            unsigned short c_id;
            unsigned int c_len;
            typedef std::ifstream::pos_type pos_type;
            _msg("loader3ds", 5, f.tellg() << ": blok materiałowy o dłg. " << end - f.tellg());
            mat m;

            while (f.tellg() < end && !f.fail()) {
                pos_type sub_end = read_chunk(f, c_id, c_len);
                if (sub_end < 0) break;

                switch (chunk_type(c_id)) {
                    case mat_name:
                    {
                        char c;
                        int i = 0;
                        do {
                            f.read(&c, 1);
                            m.name[i] = c;
                            i++;
                        } while (c != '\0' && i < 60);
                        _msg("loader3ds", 5, f.tellg() << ": załadowano nazwę materiału: " << m.name);
                    }
                        break;
                    case mat_amb:
                        m.ambient = read_color(f);
                        break;
                    case mat_spec:
                        m.specular = read_color(f);
                        break;
                    case mat_dif:
                        m.diffuse = read_color(f);
                        break;
                    case mat_shin:
                        m.shininess = read_percent(f, sub_end);
                        break;
                    case mat_trans:
                        m.transparency = read_percent(f, sub_end);
                        break;
                    case mat_2sided:
                        m.double_sided = true;
                        _msg("loader3ds", 3, "materiał " << m.name << " dwustronny");
                        break;
                    case texmap1:
                        m.tex1 = read_texmap(f, sub_end);
                        break;
                    case texmap2:
                        m.tex2 = read_texmap(f, sub_end);
                        break;
                    case op_map:
                        m.op = read_texmap(f, sub_end);
                        break;
                    case bump_map:
                        m.bump = read_texmap(f, sub_end);
                        break;
                    case shin_map:
                        m.shin = read_texmap(f, sub_end);
                        break;
                    case spec_map:
                        m.spec = read_texmap(f, sub_end);
                        break;
                    case refl_map:
                        m.refl = read_texmap(f, sub_end);
                        break;
                    default:
                        _msg("loader3ds", 1, f.tellg() << ": 0x" << std::hex << c_id << std::dec << " skok o " << c_len << " ->" << sub_end);
                        f.seekg(sub_end, std::ios_base::beg);
                        break;
                }
            }
            materials.push_back(m);
        }

        color read_color(std::ifstream & f) {
            unsigned short c_id;
            unsigned int c_len;
            color c;
            std::ifstream::pos_type end = read_chunk(f, c_id, c_len);
            switch (c_id) {
                case 0x0010: //float format
                {
                    f.read((char*) (&c.r), sizeof (float));
                    f.read((char*) (&c.g), sizeof (float));
                    f.read((char*) (&c.b), sizeof (float));
                }
                case 0x0011: //byte format
                {
                    unsigned char A, B, C;
                    f.read((char*) (&A), 1);
                    f.read((char*) (&B), 1);
                    f.read((char*) (&C), 1);
                    c.r = float(int(A) / 255.0);
                    c.g = float(int(B) / 255.0);
                    c.b = float(int(C) / 255.0);
                }
                case 0x0012: //float format gamma
                {
                    f.read((char*) (&c.r), sizeof (float));
                    f.read((char*) (&c.g), sizeof (float));
                    f.read((char*) (&c.b), sizeof (float));
                }
                case 0x0013: //byte format gamma
                {
                    unsigned char A, B, C;
                    f.read((char*) (&A), 1);
                    f.read((char*) (&B), 1);
                    f.read((char*) (&C), 1);
                    c.r = float(int(A) / 255.0);
                    c.g = float(int(B) / 255.0);
                    c.b = float(int(C) / 255.0);
                }
                default:
                    f.seekg(end, std::ios_base::beg);
                    break;
            }
            _msg("loader3ds", 3, "wczytano kolor " << c.r << ", " << c.g << ", " << c.b);
            return c;
        }

        ///wczytywanie przypisań materiału do trójkątów

        matdesc read_facemat(std::ifstream & f) {
            matdesc m;
            char c;
            int i = 0;
            do {
                f.read(&c, sizeof (char));
                m.name[i] = c;
                i++;
            } while (c != '\0' && i < 60);
            _msg("loader3ds", 5, "czytanie przypisań materiału " << m.name << " do ścian");
            f.read((char*) (&m.nfaces), sizeof (unsigned short));
            m.faces = new unsigned short[m.nfaces];
            for (int j = 0; j < m.nfaces; j++) {
                f.read((char*) (&m.faces[j]), sizeof (unsigned short));
            }
            _msg("loader3ds", 3, "wczytano przypisania materiału " << m.name << " do " << m.nfaces << " ścian");
            return m;
        }

        texmap read_texmap(std::ifstream & f, std::ifstream::pos_type end) {
            typedef std::ifstream::pos_type pos_type;
            unsigned short c_id;
            unsigned int c_len;
            texmap t;
            while (f.tellg() < end && !f.fail()) {
                pos_type sub_end = read_chunk(f, c_id, c_len);
                if (sub_end < 0) break;

                switch (c_id) {
                    case 0xA300: //nazwa tekstury
                    {
                        char c;
                        int i = 0;
                        do {
                            f.read(&c, sizeof (char));
                            t.name[i] = c;
                            i++;
                        } while (c != '\0' && i < 60);
                        _msg("loader3ds", 5, "wczytano nazwę tekstury " << t.name);
                        t.present = true;
                    }
                        break;
                    default:
                        f.seekg(sub_end, std::ios_base::beg);
                        break;
                }
            }
            return t;
        }

        std::vector<vertex> read_vertices(std::ifstream & f) {
            std::vector<vertex> ret;
            unsigned short c_qty;
            f.read((char*) (&c_qty), sizeof (unsigned short));
            for (int i = 0; i < c_qty; i++) {
                vertex v;
                f.read((char*) (&v.x), sizeof (float));
                f.read((char*) (&v.y), sizeof (float));
                f.read((char*) (&v.z), sizeof (float));
                ret.push_back(v);
            }
            _msg("loader3ds", 3, "wczytano " << ret.size() << " wierzchołków");
            return ret;
        }

        std::vector<texcoord> read_texcoords(std::ifstream & f) {
            std::vector<texcoord> ret;
            unsigned short c_qty;
            f.read((char*) (&c_qty), sizeof (unsigned short));
            for (int i = 0; i < c_qty; i++) {
                texcoord t;
                f.read((char*) (&t.u), sizeof (float));
                f.read((char*) (&t.v), sizeof (float));
                ret.push_back(t);
            }
            _msg("loader3ds", 3, "wczytano " << ret.size() << " współrzędnych tekstur");
            return ret;
        }

        std::vector<triangle> read_faces(std::ifstream & f) {
            std::vector<triangle> ret;
            unsigned short c_qty;
            unsigned short c_faceflags;
            f.read((char*) (&c_qty), sizeof (unsigned short));
            for (int i = 0; i < c_qty; i++) {
                triangle t;
                f.read((char*) (&t.c), sizeof (unsigned short));
                f.read((char*) (&t.b), sizeof (unsigned short));
                f.read((char*) (&t.a), sizeof (unsigned short));
                f.read((char*) (&c_faceflags), sizeof (unsigned short));
                ret.push_back(t);
            }
            _msg("loader3ds", 3, "wczytano " << ret.size() << " trójkątów");
            return ret;
        }

        sdl_ptr<entity> model(triangular_mesh & newmesh) {
            sdl_ptr<mesh> our_mesh(new mesh(std::string("3DS mesh: ") + newmesh.name));
            std::vector<vertex> & vertices = newmesh.vertices;
            std::vector<texcoord> & texcoords = newmesh.texcoords;
            std::vector<matdesc> & facemats = newmesh.facemats;
            std::vector<triangle> & faces = newmesh.faces;
            std::vector<color> & colors = newmesh.colors;
            colors.reserve(vertices.size());

            std::vector<unsigned int> int_idx;
            std::vector<mesh::matmap> matmaps;

            int faceidx = 0;
            int maxmat = 7;
            for (int i = 0; i < facemats.size(); i++) {
                mat * cur_mat = NULL;
                for (int k = 0; k < materials.size(); k++)
                    if (materials[k] == std::string(facemats[i].name))
                        cur_mat = &materials[k];
                if (cur_mat == NULL) {
                    cur_mat = new mat;
                    for (int i = 0; i < 10; i++)
                        cur_mat->name[i] = 'x';
                    cur_mat->name[10] = '\0';
                    cur_mat->ambient.r = 0.8;
                    cur_mat->ambient.g = 0.8;
                    cur_mat->ambient.b = 0.8;
                }
                sdl_ptr<material> our_material(new material(cur_mat->name));

                our_material->ambient = mvector4f(cur_mat->ambient.r, cur_mat->ambient.g, cur_mat->ambient.b, 1.0);
                our_material->diffuse = mvector4f(cur_mat->diffuse.r, cur_mat->diffuse.g, cur_mat->diffuse.b, 1.0);
                our_material->specular = mvector4f(cur_mat->specular.r, cur_mat->specular.g, cur_mat->specular.b, 1.0);
                our_material->shininess = cur_mat->shininess;
                our_material->diffuse[3] = 1.0f - cur_mat->transparency;
                our_material->ambient[3] = 1.0f - cur_mat->transparency;
                our_material->specular[3] = 1.0f - cur_mat->transparency;

                our_material->double_sided = cur_mat->double_sided;

                if (cur_mat->tex1.present) our_material->textures.push_back((sdl_base_texdata*) (new sdl_texdata(cur_mat->tex1.name)));
                if (cur_mat->tex2.present) our_material->textures.push_back((sdl_base_texdata*) (new sdl_texdata(cur_mat->tex2.name)));
                if (our_material->textures.size() > maxmat)
                    maxmat = our_material->textures.size();

                int mat_start = faceidx;
                int mat_end = 0;
                for (int j = 0; j < facemats[i].nfaces; j++) {
                    int_idx.push_back(faces[facemats[i].faces[j]].c);
                    int_idx.push_back(faces[facemats[i].faces[j]].b);
                    int_idx.push_back(faces[facemats[i].faces[j]].a);
                    colors[faces[facemats[i].faces[j]].c] = cur_mat->diffuse;
                    colors[faces[facemats[i].faces[j]].b] = cur_mat->diffuse;
                    colors[faces[facemats[i].faces[j]].a] = cur_mat->diffuse;
                    faceidx += 3;
                }
                mat_end = faceidx;

                matmaps.push_back(mesh::matmap(mat_start, mat_end, mat_end - mat_start + 1, our_material));
            }
            sdl_ptr<indexmap> indices(new indexmap(NULL, 0, int_idx.size()));
            for (int i = 0; i < int_idx.size(); i++)
                (*indices)[i] = int_idx[i];

            int nv = vertices.size();
            sdl_ptr<mvector3fmap> v(new mvector3fmap(nv, std::string(newmesh.name) + std::string(" vertices")));
            sdl_ptr<mvector3fmap> n(new mvector3fmap(nv, std::string(newmesh.name) + std::string(" normals")));
            sdl_ptr<mvector2fmap> t(new mvector2fmap(nv, std::string(newmesh.name) + std::string(" texcoords")));
            sdl_ptr<mvector4fmap> c(new mvector4fmap(nv, std::string(newmesh.name) + std::string(" colors")));
            for (int i = 0; i < nv; i++) {
                (*v)[i] = mvector3f(vertices[i].x, vertices[i].y, vertices[i].z);
                (*t)[i] = mvector2f(texcoords[i].u, texcoords[i].v);
                (*c)[i] = mvector4f(colors[i].r, colors[i].g, colors[i].b, colors[i].a);
            }
            std::vector<sdl_ptr<mvector2fmap> > tx;
            for (int i = 0; i < maxmat; i++)
                tx.push_back(t);

            our_mesh->create(v, n, c, tx, indices);
            our_mesh->matmaps = matmaps;
            //return our_mesh;
            //
            sdl_ptr<entity> our_entity(new entity(newmesh.name));
            our_entity->body = our_mesh;
            mvector3f x(newmesh.coord.x);
            mvector3f y(newmesh.coord.y);
            mvector3f z(newmesh.coord.z);
            mvector3f p(newmesh.coord.p);

            our_entity->transform = translation(p);
            our_entity->transform *= columns(x, y, z);
            return our_entity;

            ////TODO wektory normalne trzeba będzie chyba policzyć niestety 3ds ich nie zawiera
        }
    };


};

#endif //_sdl_loader3ds_h_

